﻿using System;
using System.Collections.Generic;
using System.Globalization;
using MathNet.Numerics.LinearAlgebra.Double;
using MathNet.Numerics.LinearAlgebra.Generic;

[assembly: CLSCompliant(true)]
namespace Marvin
{
    
    /// <summary>
    /// A training set of data points which are guaranteed to share the same number of dimensions. 
    /// </summary>
    public class TrainingSet<T> : ITrainingSet<T>
    {
        #region Fields
        private readonly int _dimensions;
        private readonly List<T> _y;
        private readonly Matrix<double> _x;
        private int _nextRow; 
        #endregion

        #region Properties
        /// <summary>
        /// NumberOfTrainingExamples of the training set / number of training examples. 
        /// </summary>
        public int NumberOfTrainingExamples { get; set; }

        /// <summary>
        /// Dimensions of the training examples. 
        /// </summary>
        public int Dimensions
        {
            get { return _dimensions; }
        }

        #endregion

        public TrainingSet(int dimensions, int numberOfTrainingExamples)
        {
            _dimensions = dimensions;
            NumberOfTrainingExamples = numberOfTrainingExamples; 
            _x = new DenseMatrix(NumberOfTrainingExamples, Dimensions);
            _y = new List<T>();
        }

        #region Behaviour 
        /// <summary>
        /// The input data as a matrix whereas the rows are transposed vectors of training examples. 
        /// </summary>
        /// <returns>two dimensional array of input data</returns>
        public Matrix<double> GetX()
        {
            return _x; 
        }

        /// <summary>
        /// The output data as a one dimensional array
        /// </summary>
        /// <returns>one dimensional array of all output data</returns>
        public T[] GetY()
        {
            return _y.ToArray();
        }

        /// <summary>
        /// Adds a training example to the training set. 
        /// </summary>
        /// <param name="x">Input data (allows duplicate values).</param>
        /// <param name="y">Result for the input data</param>
        /// <exception cref="ArgumentException">Occurs if the training example isn't initialized</exception>
        public void Add(double[] x, T y)
        {
            AddInput(x);
            AddExpectedOutput(y);
        }


        private void AddExpectedOutput(T y)
        {
            _y.Add(y);
        }

        /// <summary>
        /// Throws an exception if the training example is uninitialized.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <exception cref="System.ArgumentException">trainingExample must be initialized.</exception>
        private static void ThrowAnExceptionIfTheTrainingExampleIsUninitialized(double[] x)
        {
            if (x == null)
                throw new ArgumentException("trainingExample must be initialized.");
        }

        /// <summary>
        /// Adds the training example to the internal representation (a matrix).
        /// </summary>
        /// <param name="x">The trainingExample</param>
        private void AddInput(double[] x)
        {
            ThrowAnExceptionIfNoMoreTrainingExamplesCanBeAdded();
            ThrowAnExceptionIfTheTrainingExampleIsUninitialized(x);
            ThrowAnExceptionIfTheDimensionsDontMatch(x);
            
            for (int dimension = 0; dimension < x.Length; dimension++)
            {
                _x[_nextRow, dimension] = x[dimension];
            }

            _nextRow++; 
        }

        /// <summary>
        /// Throws an exception if the dimensions dont match.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <exception cref="System.ArgumentException">Occurs if a training example has too few or too many dimensions.</exception>
        private void ThrowAnExceptionIfTheDimensionsDontMatch(double[] x)
        {
            if (x.Length != Dimensions)
            {
                var exceptionMessage =
                    String.Format(CultureInfo.CurrentCulture,
                                  "Added training datum has {0} dimensions whilst training set has {1} dimensions.",
                                  x.Length, Dimensions);
                throw new ArgumentException(exceptionMessage);
            }
        }

        /// <summary>
        /// Throws an exception if no more training examples can be added.
        /// </summary>
        /// <exception cref="System.ArgumentException">Occurs if the the number of training examples supplied in the contructor doesn't match the actual number of training examples added.</exception>
        private void ThrowAnExceptionIfNoMoreTrainingExamplesCanBeAdded()
        {
            if (_nextRow > NumberOfTrainingExamples)
            {
                var exceptionMessage =
                    String.Format(CultureInfo.CurrentCulture,
                                  "The number of training examples is {0} and you've already added that many examples. Please use the right number of training examples in the contructor.",
                                  NumberOfTrainingExamples);
                throw new ArgumentException(exceptionMessage);
            }
        }

        #endregion
    }
}
